/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.ddmuilib.net;
import com.android.ddmlib.AdbCommandRejectedException;
import com.android.ddmlib.Client;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.MultiLineReceiver;
import com.android.ddmlib.ShellCommandUnresponsiveException;
import com.android.ddmlib.TimeoutException;
import com.android.ddmuilib.DdmUiPreferences;
import com.android.ddmuilib.TableHelper;
import com.android.ddmuilib.TablePanel;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.viewers.ILabelProviderListener;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Table;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.AxisLocation;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.DatasetRenderingOrder;
import org.jfree.chart.plot.ValueMarker;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.StackedXYAreaRenderer2;
import org.jfree.chart.renderer.xy.XYAreaRenderer;
import org.jfree.data.DefaultKeyedValues2D;
import org.jfree.data.time.Millisecond;
import org.jfree.data.time.TimePeriod;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;
import org.jfree.data.xy.AbstractIntervalXYDataset;
import org.jfree.data.xy.TableXYDataset;
import org.jfree.experimental.chart.swt.ChartComposite;
import org.jfree.ui.RectangleAnchor;
import org.jfree.ui.TextAnchor;
import java.io.IOException;
import java.text.DecimalFormat;
import java.text.FieldPosition;
import java.text.NumberFormat;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.Date;
import java.util.Formatter;
import java.util.Iterator;
/**
* Displays live network statistics for currently selected {@link Client}.
*/
public class NetworkPanel extends TablePanel {
// TODO: enable view of packets and bytes/packet
// TODO: add sash to resize chart and table
// TODO: let user edit tags to be meaningful
/** Amount of historical data to display. */
private static final long HISTORY_MILLIS = 30 * 1000;
private final static String PREFS_NETWORK_COL_TITLE = "networkPanel.title";
private final static String PREFS_NETWORK_COL_RX_BYTES = "networkPanel.rxBytes";
private final static String PREFS_NETWORK_COL_RX_PACKETS = "networkPanel.rxPackets";
private final static String PREFS_NETWORK_COL_TX_BYTES = "networkPanel.txBytes";
private final static String PREFS_NETWORK_COL_TX_PACKETS = "networkPanel.txPackets";
/** Path to network statistics on remote device. */
private static final String PROC_XT_QTAGUID = "/proc/net/xt_qtaguid/stats";
private static final java.awt.Color TOTAL_COLOR = java.awt.Color.GRAY;
/** Colors used for tag series data. */
private static final java.awt.Color[] SERIES_COLORS = new java.awt.Color[] {
java.awt.Color.decode("0x2bc4c1"), // teal
java.awt.Color.decode("0xD50F25"), // red
java.awt.Color.decode("0x3369E8"), // blue
java.awt.Color.decode("0xEEB211"), // orange
java.awt.Color.decode("0x00bd2e"), // green
java.awt.Color.decode("0xae26ae"), // purple
};
private Display mDisplay;
private Composite mPanel;
/** Header panel with configuration options. */
private Composite mHeader;
private Label mSpeedLabel;
private Combo mSpeedCombo;
/** Current sleep between each sample, from {@link #mSpeedCombo}. */
private long mSpeedMillis;
private Button mRunningButton;
private Button mResetButton;
/** Chart of recent network activity. */
private JFreeChart mChart;
private ChartComposite mChartComposite;
private ValueAxis mDomainAxis;
/** Data for total traffic (tag 0x0). */
private TimeSeriesCollection mTotalCollection;
private TimeSeries mRxTotalSeries;
private TimeSeries mTxTotalSeries;
/** Data for detailed tagged traffic. */
private LiveTimeTableXYDataset mRxDetailDataset;
private LiveTimeTableXYDataset mTxDetailDataset;
private XYAreaRenderer mTotalRenderer;
private StackedXYAreaRenderer2 mRenderer;
/** Table showing summary of network activity. */
private Table mTable;
private TableViewer mTableViewer;
/** UID of currently selected {@link Client}. */
private int mActiveUid = -1;
/** List of traffic flows being actively tracked. */
private ArrayList<TrackedItem> mTrackedItems = new ArrayList<TrackedItem>();
private SampleThread mSampleThread;
private class SampleThread extends Thread {
private volatile boolean mFinish;
public void finish() {
mFinish = true;
interrupt();
}
@Override
public void run() {
while (!mFinish && !mDisplay.isDisposed()) {
performSample();
try {
Thread.sleep(mSpeedMillis);
} catch (InterruptedException e) {
// ignored
}
}
}
}
/** Last snapshot taken by {@link #performSample()}. */
private NetworkSnapshot mLastSnapshot;
@Override
protected Control createControl(Composite parent) {
mDisplay = parent.getDisplay();
mPanel = new Composite(parent, SWT.NONE);
final FormLayout formLayout = new FormLayout();
mPanel.setLayout(formLayout);
createHeader();
createChart();
createTable();
return mPanel;
}
/**
* Create header panel with configuration options.
*/
private void createHeader() {
mHeader = new Composite(mPanel, SWT.NONE);
final RowLayout layout = new RowLayout();
layout.center = true;
mHeader.setLayout(layout);
mSpeedLabel = new Label(mHeader, SWT.NONE);
mSpeedLabel.setText("Speed:");
mSpeedCombo = new Combo(mHeader, SWT.PUSH);
mSpeedCombo.add("Fast (100ms)");
mSpeedCombo.add("Medium (250ms)");
mSpeedCombo.add("Slow (500ms)");
mSpeedCombo.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
updateSpeed();
}
});
mSpeedCombo.select(1);
updateSpeed();
mRunningButton = new Button(mHeader, SWT.PUSH);
mRunningButton.setText("Start");
mRunningButton.setEnabled(false);
mRunningButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
final boolean alreadyRunning = mSampleThread != null;
updateRunning(!alreadyRunning);
}
});
mResetButton = new Button(mHeader, SWT.PUSH);
mResetButton.setText("Reset");
mResetButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
clearTrackedItems();
}
});
final FormData data = new FormData();
data.top = new FormAttachment(0);
data.left = new FormAttachment(0);
data.right = new FormAttachment(100);
mHeader.setLayoutData(data);
}
/**
* Create chart of recent network activity.
*/
private void createChart() {
mChart = ChartFactory.createTimeSeriesChart(null, null, null, null, false, false, false);
// create backing datasets and series
mRxTotalSeries = new TimeSeries("RX total");
mTxTotalSeries = new TimeSeries("TX total");
mRxTotalSeries.setMaximumItemAge(HISTORY_MILLIS);
mTxTotalSeries.setMaximumItemAge(HISTORY_MILLIS);
mTotalCollection = new TimeSeriesCollection();
mTotalCollection.addSeries(mRxTotalSeries);
mTotalCollection.addSeries(mTxTotalSeries);
mRxDetailDataset = new LiveTimeTableXYDataset();
mTxDetailDataset = new LiveTimeTableXYDataset();
mTotalRenderer = new XYAreaRenderer(XYAreaRenderer.AREA);
mRenderer = new StackedXYAreaRenderer2();
final XYPlot xyPlot = mChart.getXYPlot();
xyPlot.setDatasetRenderingOrder(DatasetRenderingOrder.FORWARD);
xyPlot.setDataset(0, mTotalCollection);
xyPlot.setDataset(1, mRxDetailDataset);
xyPlot.setDataset(2, mTxDetailDataset);
xyPlot.setRenderer(0, mTotalRenderer);
xyPlot.setRenderer(1, mRenderer);
xyPlot.setRenderer(2, mRenderer);
// we control domain axis manually when taking samples
mDomainAxis = xyPlot.getDomainAxis();
mDomainAxis.setAutoRange(false);
final NumberAxis axis = new NumberAxis();
axis.setNumberFormatOverride(new BytesFormat(true));
axis.setAutoRangeMinimumSize(50);
xyPlot.setRangeAxis(axis);
xyPlot.setRangeAxisLocation(AxisLocation.BOTTOM_OR_RIGHT);
// draw thick line to separate RX versus TX traffic
xyPlot.addRangeMarker(
new ValueMarker(0, java.awt.Color.BLACK, new java.awt.BasicStroke(2)));
// label to indicate that positive axis is RX traffic
final ValueMarker rxMarker = new ValueMarker(0);
rxMarker.setStroke(new java.awt.BasicStroke(0));
rxMarker.setLabel("RX");
rxMarker.setLabelFont(rxMarker.getLabelFont().deriveFont(30f));
rxMarker.setLabelPaint(java.awt.Color.LIGHT_GRAY);
rxMarker.setLabelAnchor(RectangleAnchor.TOP_RIGHT);
rxMarker.setLabelTextAnchor(TextAnchor.BOTTOM_RIGHT);
xyPlot.addRangeMarker(rxMarker);
// label to indicate that negative axis is TX traffic
final ValueMarker txMarker = new ValueMarker(0);
txMarker.setStroke(new java.awt.BasicStroke(0));
txMarker.setLabel("TX");
txMarker.setLabelFont(txMarker.getLabelFont().deriveFont(30f));
txMarker.setLabelPaint(java.awt.Color.LIGHT_GRAY);
txMarker.setLabelAnchor(RectangleAnchor.BOTTOM_RIGHT);
txMarker.setLabelTextAnchor(TextAnchor.TOP_RIGHT);
xyPlot.addRangeMarker(txMarker);
mChartComposite = new ChartComposite(mPanel, SWT.BORDER, mChart,
ChartComposite.DEFAULT_WIDTH, ChartComposite.DEFAULT_HEIGHT,
ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH,
ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT, 4096, 4096, true, true, true, true,
false, true);
final FormData data = new FormData();
data.top = new FormAttachment(mHeader);
data.left = new FormAttachment(0);
data.bottom = new FormAttachment(70);
data.right = new FormAttachment(100);
mChartComposite.setLayoutData(data);
}
/**
* Create table showing summary of network activity.
*/
private void createTable() {
mTable = new Table(mPanel, SWT.BORDER | SWT.MULTI | SWT.FULL_SELECTION);
final FormData data = new FormData();
data.top = new FormAttachment(mChartComposite);
data.left = new FormAttachment(mChartComposite, 0, SWT.CENTER);
data.bottom = new FormAttachment(100);
mTable.setLayoutData(data);
mTable.setHeaderVisible(true);
mTable.setLinesVisible(true);
final IPreferenceStore store = DdmUiPreferences.getStore();
TableHelper.createTableColumn(mTable, "", SWT.CENTER, buildSampleText(2), null, null);
TableHelper.createTableColumn(
mTable, "Tag", SWT.LEFT, buildSampleText(32), PREFS_NETWORK_COL_TITLE, store);
TableHelper.createTableColumn(mTable, "RX bytes", SWT.RIGHT, buildSampleText(12),
PREFS_NETWORK_COL_RX_BYTES, store);
TableHelper.createTableColumn(mTable, "RX packets", SWT.RIGHT, buildSampleText(12),
PREFS_NETWORK_COL_RX_PACKETS, store);
TableHelper.createTableColumn(mTable, "TX bytes", SWT.RIGHT, buildSampleText(12),
PREFS_NETWORK_COL_TX_BYTES, store);
TableHelper.createTableColumn(mTable, "TX packets", SWT.RIGHT, buildSampleText(12),
PREFS_NETWORK_COL_TX_PACKETS, store);
mTableViewer = new TableViewer(mTable);
mTableViewer.setContentProvider(new ContentProvider());
mTableViewer.setLabelProvider(new LabelProvider());
}
/**
* Update {@link #mSpeedMillis} to match {@link #mSpeedCombo} selection.
*/
private void updateSpeed() {
switch (mSpeedCombo.getSelectionIndex()) {
case 0:
mSpeedMillis = 100;
break;
case 1:
mSpeedMillis = 250;
break;
case 2:
mSpeedMillis = 500;
break;
}
}
/**
* Update if {@link SampleThread} should be actively running. Will create
* new thread or finish existing thread to match requested state.
*/
private void updateRunning(boolean shouldRun) {
final boolean alreadyRunning = mSampleThread != null;
if (alreadyRunning && !shouldRun) {
mSampleThread.finish();
mSampleThread = null;
mRunningButton.setText("Start");
mHeader.pack();
} else if (!alreadyRunning && shouldRun) {
mSampleThread = new SampleThread();
mSampleThread.start();
mRunningButton.setText("Stop");
mHeader.pack();
}
}
@Override
public void setFocus() {
mPanel.setFocus();
}
private static java.awt.Color nextSeriesColor(int index) {
return SERIES_COLORS[index % SERIES_COLORS.length];
}
/**
* Find a {@link TrackedItem} that matches the requested UID and tag, or
* create one if none exists.
*/
public TrackedItem findOrCreateTrackedItem(int uid, int tag) {
// try searching for existing item
for (TrackedItem item : mTrackedItems) {
if (item.uid == uid && item.tag == tag) {
return item;
}
}
// nothing found; create new item
final TrackedItem item = new TrackedItem(uid, tag);
if (item.isTotal()) {
item.color = TOTAL_COLOR;
item.label = "Total";
} else {
final int size = mTrackedItems.size();
item.color = nextSeriesColor(size);
Formatter formatter = new Formatter();
item.label = "0x" + formatter.format("%08x", tag);
formatter.close();
}
// create color chip to display as legend in table
item.colorImage = new Image(mDisplay, 20, 20);
final GC gc = new GC(item.colorImage);
gc.setBackground(new org.eclipse.swt.graphics.Color(mDisplay, item.color
.getRed(), item.color.getGreen(), item.color.getBlue()));
gc.fillRectangle(item.colorImage.getBounds());
gc.dispose();
mTrackedItems.add(item);
return item;
}
/**
* Clear all {@link TrackedItem} and chart history.
*/
public void clearTrackedItems() {
mRxTotalSeries.clear();
mTxTotalSeries.clear();
mRxDetailDataset.clear();
mTxDetailDataset.clear();
mTrackedItems.clear();
mTableViewer.setInput(mTrackedItems);
}
/**
* Update the {@link #mRenderer} colors to match {@link TrackedItem#color}.
*/
private void updateSeriesPaint() {
for (TrackedItem item : mTrackedItems) {
final int seriesIndex = mRxDetailDataset.getColumnIndex(item.label);
if (seriesIndex >= 0) {
mRenderer.setSeriesPaint(seriesIndex, item.color);
mRenderer.setSeriesFillPaint(seriesIndex, item.color);
}
}
// series data is always the same color
final int count = mTotalCollection.getSeriesCount();
for (int i = 0; i < count; i++) {
mTotalRenderer.setSeriesPaint(i, TOTAL_COLOR);
mTotalRenderer.setSeriesFillPaint(i, TOTAL_COLOR);
}
}
/**
* Traffic flow being actively tracked, uniquely defined by UID and tag. Can
* record {@link NetworkSnapshot} deltas into {@link TimeSeries} for
* charting, and into summary statistics for {@link Table} display.
*/
private class TrackedItem {
public final int uid;
public final int tag;
public java.awt.Color color;
public Image colorImage;
public String label;
public long rxBytes;
public long rxPackets;
public long txBytes;
public long txPackets;
public TrackedItem(int uid, int tag) {
this.uid = uid;
this.tag = tag;
}
public boolean isTotal() {
return tag == 0x0;
}
/**
* Record the given {@link NetworkSnapshot} delta, updating
* {@link TimeSeries} and summary statistics.
*
* @param time Timestamp when delta was observed.
* @param deltaMillis Time duration covered by delta, in milliseconds.
*/
public void recordDelta(Millisecond time, long deltaMillis, NetworkSnapshot.Entry delta) {
final long rxBytesPerSecond = (delta.rxBytes * 1000) / deltaMillis;
final long txBytesPerSecond = (delta.txBytes * 1000) / deltaMillis;
// record values under correct series
if (isTotal()) {
mRxTotalSeries.addOrUpdate(time, rxBytesPerSecond);
mTxTotalSeries.addOrUpdate(time, -txBytesPerSecond);
} else {
mRxDetailDataset.addValue(rxBytesPerSecond, time, label);
mTxDetailDataset.addValue(-txBytesPerSecond, time, label);
}
rxBytes += delta.rxBytes;
rxPackets += delta.rxPackets;
txBytes += delta.txBytes;
txPackets += delta.txPackets;
}
}
@Override
public void deviceSelected() {
// treat as client selection to update enabled states
clientSelected();
}
@Override
public void clientSelected() {
mActiveUid = -1;
final Client client = getCurrentClient();
if (client != null) {
final int pid = client.getClientData().getPid();
try {
// map PID to UID from device
final UidParser uidParser = new UidParser();
getCurrentDevice().executeShellCommand("cat /proc/" + pid + "/status", uidParser);
mActiveUid = uidParser.uid;
} catch (TimeoutException e) {
e.printStackTrace();
} catch (AdbCommandRejectedException e) {
e.printStackTrace();
} catch (ShellCommandUnresponsiveException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
clearTrackedItems();
updateRunning(false);
final boolean validUid = mActiveUid != -1;
mRunningButton.setEnabled(validUid);
}
@Override
public void clientChanged(Client client, int changeMask) {
// ignored
}
/**
* Take a snapshot from {@link #getCurrentDevice()}, recording any delta
* network traffic to {@link TrackedItem}.
*/
public void performSample() {
final IDevice device = getCurrentDevice();
if (device == null) return;
try {
final NetworkSnapshotParser parser = new NetworkSnapshotParser();
device.executeShellCommand("cat " + PROC_XT_QTAGUID, parser);
if (parser.isError()) {
mDisplay.asyncExec(new Runnable() {
@Override
public void run() {
updateRunning(false);
final String title = "Problem reading stats";
final String message = "Problem reading xt_qtaguid network "
+ "statistics from selected device.";
Status status = new Status(IStatus.ERROR, "NetworkPanel", 0, message, null);
ErrorDialog.openError(mPanel.getShell(), title, title, status);
}
});
return;
}
final NetworkSnapshot snapshot = parser.getParsedSnapshot();
// use first snapshot as baseline
if (mLastSnapshot == null) {
mLastSnapshot = snapshot;
return;
}
final NetworkSnapshot delta = NetworkSnapshot.subtract(snapshot, mLastSnapshot);
mLastSnapshot = snapshot;
// perform delta updates over on UI thread
if (!mDisplay.isDisposed()) {
mDisplay.syncExec(new UpdateDeltaRunnable(delta, snapshot.timestamp));
}
} catch (TimeoutException e) {
e.printStackTrace();
} catch (AdbCommandRejectedException e) {
e.printStackTrace();
} catch (ShellCommandUnresponsiveException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Task that updates UI with given {@link NetworkSnapshot} delta.
*/
private class UpdateDeltaRunnable implements Runnable {
private final NetworkSnapshot mDelta;
private final long mEndTime;
public UpdateDeltaRunnable(NetworkSnapshot delta, long endTime) {
mDelta = delta;
mEndTime = endTime;
}
@Override
public void run() {
if (mDisplay.isDisposed()) return;
final Millisecond time = new Millisecond(new Date(mEndTime));
for (NetworkSnapshot.Entry entry : mDelta) {
if (mActiveUid != entry.uid) continue;
final TrackedItem item = findOrCreateTrackedItem(entry.uid, entry.tag);
item.recordDelta(time, mDelta.timestamp, entry);
}
// remove any historical detail data
final long beforeMillis = mEndTime - HISTORY_MILLIS;
mRxDetailDataset.removeBefore(beforeMillis);
mTxDetailDataset.removeBefore(beforeMillis);
// trigger refresh from bulk changes above
mRxDetailDataset.fireDatasetChanged();
mTxDetailDataset.fireDatasetChanged();
// update axis to show latest 30 second time period
mDomainAxis.setRange(mEndTime - HISTORY_MILLIS, mEndTime);
updateSeriesPaint();
// kick table viewer to update
mTableViewer.setInput(mTrackedItems);
}
}
/**
* Parser that extracts UID from remote {@code /proc/pid/status} file.
*/
private static class UidParser extends MultiLineReceiver {
public int uid = -1;
@Override
public boolean isCancelled() {
return false;
}
@Override
public void processNewLines(String[] lines) {
for (String line : lines) {
if (line.startsWith("Uid:")) {
// we care about the "real" UID
final String[] cols = line.split("\t");
uid = Integer.parseInt(cols[1]);
}
}
}
}
/**
* Parser that populates {@link NetworkSnapshot} based on contents of remote
* {@link NetworkPanel#PROC_XT_QTAGUID} file.
*/
private static class NetworkSnapshotParser extends MultiLineReceiver {
private NetworkSnapshot mSnapshot;
public NetworkSnapshotParser() {
mSnapshot = new NetworkSnapshot(System.currentTimeMillis());
}
public boolean isError() {
return mSnapshot == null;
}
public NetworkSnapshot getParsedSnapshot() {
return mSnapshot;
}
@Override
public boolean isCancelled() {
return false;
}
@Override
public void processNewLines(String[] lines) {
for (String line : lines) {
if (line.endsWith("No such file or directory")) {
mSnapshot = null;
return;
}
// ignore header line
if (line.startsWith("idx")) {
continue;
}
final String[] cols = line.split(" ");
if (cols.length < 9) continue;
// iface and set are currently ignored, which groups those
// entries together.
final NetworkSnapshot.Entry entry = new NetworkSnapshot.Entry();
entry.iface = null; //cols[1];
entry.uid = Integer.parseInt(cols[3]);
entry.set = -1; //Integer.parseInt(cols[4]);
entry.tag = kernelToTag(cols[2]);
entry.rxBytes = Long.parseLong(cols[5]);
entry.rxPackets = Long.parseLong(cols[6]);
entry.txBytes = Long.parseLong(cols[7]);
entry.txPackets = Long.parseLong(cols[8]);
mSnapshot.combine(entry);
}
}
/**
* Convert {@code /proc/} tag format to {@link Integer}. Assumes incoming
* format like {@code 0x7fffffff00000000}.
* Matches code in android.server.NetworkManagementSocketTagger
*/
public static int kernelToTag(String string) {
int length = string.length();
if (length > 10) {
return Long.decode(string.substring(0, length - 8)).intValue();
} else {
return 0;
}
}
}
/**
* Parsed snapshot of {@link NetworkPanel#PROC_XT_QTAGUID} at specific time.
*/
private static class NetworkSnapshot implements Iterable<NetworkSnapshot.Entry> {
private ArrayList<Entry> mStats = new ArrayList<Entry>();
public final long timestamp;
/** Single parsed statistics row. */
public static class Entry {
public String iface;
public int uid;
public int set;
public int tag;
public long rxBytes;
public long rxPackets;
public long txBytes;
public long txPackets;
public boolean isEmpty() {
return rxBytes == 0 && rxPackets == 0 && txBytes == 0 && txPackets == 0;
}
}
public NetworkSnapshot(long timestamp) {
this.timestamp = timestamp;
}
public void clear() {
mStats.clear();
}
/**
* Combine the given {@link Entry} with any existing {@link Entry}, or
* insert if none exists.
*/
public void combine(Entry entry) {
final Entry existing = findEntry(entry.iface, entry.uid, entry.set, entry.tag);
if (existing != null) {
existing.rxBytes += entry.rxBytes;
existing.rxPackets += entry.rxPackets;
existing.txBytes += entry.txBytes;
existing.txPackets += entry.txPackets;
} else {
mStats.add(entry);
}
}
@Override
public Iterator<Entry> iterator() {
return mStats.iterator();
}
public Entry findEntry(String iface, int uid, int set, int tag) {
for (Entry entry : mStats) {
if (entry.uid == uid && entry.set == set && entry.tag == tag
&& equal(entry.iface, iface)) {
return entry;
}
}
return null;
}
/**
* Subtract the two given {@link NetworkSnapshot} objects, returning the
* delta between them.
*/
public static NetworkSnapshot subtract(NetworkSnapshot left, NetworkSnapshot right) {
final NetworkSnapshot result = new NetworkSnapshot(left.timestamp - right.timestamp);
// for each row on left, subtract value from right side
for (Entry leftEntry : left) {
final Entry rightEntry = right.findEntry(
leftEntry.iface, leftEntry.uid, leftEntry.set, leftEntry.tag);
if (rightEntry == null) continue;
final Entry resultEntry = new Entry();
resultEntry.iface = leftEntry.iface;
resultEntry.uid = leftEntry.uid;
resultEntry.set = leftEntry.set;
resultEntry.tag = leftEntry.tag;
resultEntry.rxBytes = leftEntry.rxBytes - rightEntry.rxBytes;
resultEntry.rxPackets = leftEntry.rxPackets - rightEntry.rxPackets;
resultEntry.txBytes = leftEntry.txBytes - rightEntry.txBytes;
resultEntry.txPackets = leftEntry.txPackets - rightEntry.txPackets;
result.combine(resultEntry);
}
return result;
}
}
/**
* Provider of {@link #mTrackedItems}.
*/
private class ContentProvider implements IStructuredContentProvider {
@Override
public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
// pass
}
@Override
public void dispose() {
// pass
}
@Override
public Object[] getElements(Object inputElement) {
return mTrackedItems.toArray();
}
}
/**
* Provider of labels for {@Link TrackedItem} values.
*/
private static class LabelProvider implements ITableLabelProvider {
private final DecimalFormat mFormat = new DecimalFormat("#,###");
@Override
public Image getColumnImage(Object element, int columnIndex) {
if (element instanceof TrackedItem) {
final TrackedItem item = (TrackedItem) element;
switch (columnIndex) {
case 0:
return item.colorImage;
}
}
return null;
}
@Override
public String getColumnText(Object element, int columnIndex) {
if (element instanceof TrackedItem) {
final TrackedItem item = (TrackedItem) element;
switch (columnIndex) {
case 0:
return null;
case 1:
return item.label;
case 2:
return mFormat.format(item.rxBytes);
case 3:
return mFormat.format(item.rxPackets);
case 4:
return mFormat.format(item.txBytes);
case 5:
return mFormat.format(item.txPackets);
}
}
return null;
}
@Override
public void addListener(ILabelProviderListener listener) {
// pass
}
@Override
public void dispose() {
// pass
}
@Override
public boolean isLabelProperty(Object element, String property) {
// pass
return false;
}
@Override
public void removeListener(ILabelProviderListener listener) {
// pass
}
}
/**
* Format that displays simplified byte units for when given values are
* large enough.
*/
private static class BytesFormat extends NumberFormat {
private final String[] mUnits;
private final DecimalFormat mFormat = new DecimalFormat("#.#");
public BytesFormat(boolean perSecond) {
if (perSecond) {
mUnits = new String[] { "B/s", "KB/s", "MB/s" };
} else {
mUnits = new String[] { "B", "KB", "MB" };
}
}
@Override
public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) {
double value = Math.abs(number);
int i = 0;
while (value > 1024 && i < mUnits.length - 1) {
value /= 1024;
i++;
}
toAppendTo.append(mFormat.format(value));
toAppendTo.append(mUnits[i]);
return toAppendTo;
}
@Override
public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) {
return format((long) number, toAppendTo, pos);
}
@Override
public Number parse(String source, ParsePosition parsePosition) {
return null;
}
}
public static boolean equal(Object a, Object b) {
return a == b || (a != null && a.equals(b));
}
/**
* Build stub string of requested length, usually for measurement.
*/
private static String buildSampleText(int length) {
final StringBuilder builder = new StringBuilder(length);
for (int i = 0; i < length; i++) {
builder.append("X");
}
return builder.toString();
}
/**
* Dataset that contains live measurements. Exposes
* {@link #removeBefore(long)} to efficiently remove old data, and enables
* batched {@link #fireDatasetChanged()} events.
*/
public static class LiveTimeTableXYDataset extends AbstractIntervalXYDataset implements
TableXYDataset {
private DefaultKeyedValues2D mValues = new DefaultKeyedValues2D(true);
/**
* Caller is responsible for triggering {@link #fireDatasetChanged()}.
*/
public void addValue(Number value, TimePeriod rowKey, String columnKey) {
mValues.addValue(value, rowKey, columnKey);
}
/**
* Caller is responsible for triggering {@link #fireDatasetChanged()}.
*/
public void removeBefore(long beforeMillis) {
while(mValues.getRowCount() > 0) {
final TimePeriod period = (TimePeriod) mValues.getRowKey(0);
if (period.getEnd().getTime() < beforeMillis) {
mValues.removeRow(0);
} else {
break;
}
}
}
public int getColumnIndex(String key) {
return mValues.getColumnIndex(key);
}
public void clear() {
mValues.clear();
fireDatasetChanged();
}
@Override
public void fireDatasetChanged() {
super.fireDatasetChanged();
}
@Override
public int getItemCount() {
return mValues.getRowCount();
}
@Override
public int getItemCount(int series) {
return mValues.getRowCount();
}
@Override
public int getSeriesCount() {
return mValues.getColumnCount();
}
@Override
public Comparable getSeriesKey(int series) {
return mValues.getColumnKey(series);
}
@Override
public double getXValue(int series, int item) {
final TimePeriod period = (TimePeriod) mValues.getRowKey(item);
return period.getStart().getTime();
}
@Override
public double getStartXValue(int series, int item) {
return getXValue(series, item);
}
@Override
public double getEndXValue(int series, int item) {
return getXValue(series, item);
}
@Override
public Number getX(int series, int item) {
return getXValue(series, item);
}
@Override
public Number getStartX(int series, int item) {
return getXValue(series, item);
}
@Override
public Number getEndX(int series, int item) {
return getXValue(series, item);
}
@Override
public Number getY(int series, int item) {
return mValues.getValue(item, series);
}
@Override
public Number getStartY(int series, int item) {
return getY(series, item);
}
@Override
public Number getEndY(int series, int item) {
return getY(series, item);
}
}
}